並查集又稱不相交集資料結構,其實是之前討論過的資料樹的延伸。剛開始的樹每一個都是獨立的,一棵樹只有一個節點。在透過尋找相同的根節點 (root),來將這些樹逐漸合併的過程,最終成為一棵完整的樹。就讓我們用下面的例子來一一解釋整個過程。
問題是這樣的。在班上每個人都是獨立的,但總會有很多的小團體。今天你是老師,為了要認識學生,就要先找到每個小團體中的主要人物。可是你不知道他們是誰,你只得到零碎的一些訊息,比如誰跟誰比較好等等。而我們必須要把這些訊息一一合併歸類,最終找到誰屬於哪個團體,誰是其中的主要人物。
首先我們知道班上有 10 個學生,而你有 9 條線索,每個線索都是倆倆相對的。
students = 10
leads = 9
clues = [[1, 2], [3, 4], [5, 2], [4, 6], [2, 6], [8, 7], [9, 7], [1, 6], [2, 4]]
我們先設一個一維陣列,裡面有全部的學生的號碼。
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
f[id] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
我們跟著線索慢慢的一步一步找。第一個線索是 1 號跟 2 號很要好。根據靠左原則,把左邊的當小團體的頭頭, | ||||||||||
我們把 id = 2 的值改為 1,這樣就代表 1 號和 2 號同屬一個團體。 | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 1 | 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
接著是 3 號跟 4 號,同理,根據靠左原則,把 id = 4 的值改為 3 。 | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 1 | 1 | 3 | 3 | 5 | 6 | 7 | 8 | 9 | 10 |
再來是 5 號跟 2 號,這就表示 1 號跟 2 號、5 號都屬同字個團體的。這時問題就出現了, id = 2 的值本身已經為 1 了,但我們一樣根據靠左原則,把 id = 2 的值改為 5 。但在那之前,參照古人說擒賊先擒王的概念,我們要先把原本的頭頭 id = 1 的值先改為 5,就像f[f[2]] = f[5] 這樣,之後再把 id = 2 的值改為 5。(為什麼要這樣,可以自己想一下。) |
所以這時候的狀態變成:
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
f[id] | 5 | 5 | 3 | 3 | 5 | 6 | 7 | 8 | 9 | 10 |
接著是 4 號跟 6 號: | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 3 | 3 | 5 | 3 | 7 | 8 | 9 | 10 |
再來是 2 號跟 6 號,但 2 號本身的值(f[2])是 5,而 6 號的值則是 3 ,根據靠左原則,6 號要改成 2 號的值沒錯,但還有擒賊先擒王的概念在,我們要先改 3 號的值改成 5,才能接著將 6 號的值跟著改為 5。 | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 5 | 3 | 5 | 5 | 7 | 8 | 9 | 10 |
接著是 8 號跟 7 號: | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 5 | 3 | 5 | 5 | 8 | 8 | 9 | 10 |
9 號跟 7 號: | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 5 | 3 | 5 | 5 | 9 | 9 | 9 | 10 |
1 號跟 6 號,因為這兩個號碼都已經是屬於 5 的團體,這條線索就是冗餘的。 | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 5 | 3 | 5 | 5 | 9 | 9 | 9 | 10 |
2 號跟 4 號,但其實 f[2] = 5,f[4] = 3,而 f[3]又是屬於 5 的,最後發現其實 2, 3, 4 都是屬於同一個小團體,所以這條線索也是冗餘的。 | ||||||||||
id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
- | - | - | - | - | - | - | - | - | - | - |
f[id] | 5 | 5 | 5 | 3 | 5 | 5 | 9 | 9 | 9 | 10 |
最後再找出有幾個集團就行了~ |
總結幾項特點:
# people = 人數; leads = 線索數
people, leads = map(int, input().split(' '))
# 初始化每個人的編號
f = []
for i in range(people):
f.append(i)
# 「擒賊先擒王原則」,不斷的找,找到小團體裡面的頭為止。
def getf(f, v):
if f[v] == v:
return v
else:
# 路徑壓縮 (path compression),每次在函數返回的時候,
# 順道把路上遇到的「頭頭」改為最後找到的「大頭編號」,也就是小團體的主要人物。
# 這樣可以提高以後找主要人物的速度。
f[v] = getf(f, f[v])
return f[v]
# 合併兩個小團體的函數
def merge(f, v, u):
t1 = getf(f, v)
t2 = getf(f, u)
if t1 != t2 : # 判斷兩位學生是否同屬一個小團體,及是否同一個頭頭。
f[t2] = t1
# 「靠左原則」,左邊變成右邊的頭頭。即把右邊的集合,變成左邊的子集合。
# 經過路徑壓縮以後,將f[u]的根的值也指定為v的大頭f[t1]
'''
測試資料 (test data) :
10 9
1 2
3 4
5 2
4 6
2 6
8 7
9 7
1 6
2 4
'''
# 開始合併線索
for i in range(leads):
clue1, clue2 = map(int, input().split(' '))
merge(f, clue1, clue2)
# 掃描最後有幾個獨立的小團體
sum = 0
for j in range(people):
if f[j] == j :
sum += 1
print(sum)
'''
answer = 3
'''